﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Globalization;

namespace KML
{
    public class KMLDocument
    {
        private string name;
        private List<KMLFolder> folders;

        private KMLDocument(string name, List<KMLFolder> folders)
        {
            this.folders = folders;
            this.name = name;
        }

        public string Name { get { return name; } }

        public static KMLDocument Load(XmlReader xr)
        {
            XmlDocument xdoc = new XmlDocument();
            xdoc.Load(xr);

            XmlNamespaceManager nmspc = new XmlNamespaceManager(xdoc.NameTable);
            XmlElement root = xdoc.DocumentElement;
            if (root.Name != "Document")
            {
                if (root.Name != "kml" || 
                    !root.NamespaceURI.StartsWith("http://earth.google.com/kml"))
                    throw new Exception("KML document element not found");

                nmspc.AddNamespace(String.Empty, root.NamespaceURI);
            }
            nmspc.AddNamespace("ns", root.NamespaceURI);

            string name = "kml";
            List<KMLFolder> folders = ReadFolder(root, nmspc, name);
            return new KMLDocument(name, folders);
        }

        public static KMLDocument Load(string uri)
        {
            XmlReader xr = XmlReader.Create(uri);
            KMLDocument kml = Load(xr);
            xr.Close();
            return kml;
        }

        public static KMLDocument LoadFromString(string content)
        {
            System.IO.StringReader sr = new System.IO.StringReader(content);
            XmlReader xr = XmlReader.Create(sr);
            KMLDocument kml = Load(xr);
            xr.Close();
            return kml;
        }

        private static List<KMLFolder> ReadFolder(XmlNode element, XmlNamespaceManager nmspc, string rootName)
        {
            List<KMLFolder> folders = new List<KMLFolder>();

            foreach (XmlNode child in element.SelectNodes("ns:Folder", nmspc))
            {
                XmlNode node = child.SelectSingleNode("ns:name", nmspc);
                if (node == null)
                    throw new Exception("Folder has no name");
                string name = node.InnerText;

                string fname = rootName + "/" + name;

                List<KMLFolder> subfolders = ReadFolder(child, nmspc, fname);

                string descr = "";
                node = child.SelectSingleNode("ns:description", nmspc);
                if (node != null)
                    descr = node.InnerXml;

                float vis = 1;
                node = child.SelectSingleNode("ns:visibility", nmspc);
                if (node != null)
                    vis = float.Parse(node.InnerText);

                List<KMLPlacemark> placemarks = new List<KMLPlacemark>();
                foreach (XmlNode placemark in child.SelectNodes("ns:Placemark", nmspc))
                {
                    placemarks.Add(ReadPlacemark(placemark, nmspc, fname));
                }

                List<KMLNetworkLink> nlinks = new List<KMLNetworkLink>();
                foreach (XmlNode nlink in child.SelectNodes("ns:NetworkLink", nmspc))
                {
                    nlinks.Add(ReadNetworkLink(nlink, nmspc, fname));
                }

                KMLFolder folder = new KMLFolder(name, descr, vis, subfolders, placemarks, nlinks, rootName);
                folders.Add(folder);
            }
            return folders;
        }

        private static KMLNetworkLink ReadNetworkLink(XmlNode nl, XmlNamespaceManager nmspc, string rootName)
        {
            XmlNode node = nl.SelectSingleNode("ns:name", nmspc);
            if (node == null)
                throw new Exception("NetworkLink has no name");
            string name = node.InnerText;

            XmlNode url = nl.SelectSingleNode("ns:Link", nmspc);
            KMLNetworkLinkUrl nlUrl = ReadNetworkLinkUrl(url, nmspc);

            return new KMLNetworkLink(name, nlUrl, rootName);
        }

        private static KMLNetworkLinkUrl ReadNetworkLinkUrl(XmlNode pointNode, XmlNamespaceManager nmspc)
        {
            XmlNode node = pointNode.SelectSingleNode("ns:href", nmspc);
            if (node == null)
                throw new Exception("NetworkLinkUrl has no href");
            string href = node.InnerText;

            node = pointNode.SelectSingleNode("ns:viewRefreshMode", nmspc);
            if (node == null)
                throw new Exception("NetworkLinkUrl has no viewRefreshMode");
            ViewRefreshMode mode = GetMode(node.InnerText);

            int time = 4;
            node = pointNode.SelectSingleNode("ns:viewRefreshTime", nmspc);
            if (node != null)
                time = int.Parse(node.InnerText);

            return new KMLNetworkLinkUrl(href, mode, time);
        }

        private static ViewRefreshMode GetMode(string mode)
        {
            switch (mode.ToLower())
            {
                case "onstop": return ViewRefreshMode.OnStop;
                default:
                    throw new Exception("Unknown viewRefreshMode: " + mode);
            }
        }

        private static KMLPlacemark ReadPlacemark(XmlNode placemark, XmlNamespaceManager nmspc, string rootName)
        {
            XmlNode node = placemark.SelectSingleNode("ns:name", nmspc);
            if (node == null)
                throw new Exception("Placemark has no name");
            string name = node.InnerText;

            string descr = "";
            node = placemark.SelectSingleNode("ns:description", nmspc);
            if (node != null)
                descr = node.InnerText;

            node = placemark.SelectSingleNode("ns:Point", nmspc);
            if (node == null)
                throw new Exception("Placemark has no point");
            KMLPoint point = ReadPoint(node, nmspc);

            KMLIcon? icon = null;
            node = placemark.SelectSingleNode("ns:Style/ns:IconStyle", nmspc);
            if (node != null)
                icon = ReadIcon(node, nmspc);

            return new KMLPlacemark(name, descr, "", point, icon, rootName);
        }

        private static KMLIcon ReadIcon(XmlNode iconStyleNode, XmlNamespaceManager nmspc)
        {
            XmlNode icon = iconStyleNode.SelectSingleNode("ns:Icon", nmspc);
            if (icon == null)
                throw new Exception("Placemark IconStyle has no Icon");

            XmlNode node = icon.SelectSingleNode("ns:href", nmspc);
            if (node == null)
                throw new Exception("Placemark icon has no href");
            string href = node.InnerText;

            return new KMLIcon(href);
        }

        private static KMLPoint ReadPoint(XmlNode pointNode, XmlNamespaceManager nmspc)
        {
            XmlNode node = pointNode.SelectSingleNode("ns:altitudeMode", nmspc);
            AltitudeMode altMode;
            if (node == null)
                altMode = AltitudeMode.RelativeToGround;
            else
            {
                string saltMode = node.InnerText;
                altMode = GetAltMode(saltMode);
            }

            node = pointNode.SelectSingleNode("ns:coordinates", nmspc);
            if (node == null)
                throw new Exception("Placemark point has no coordinates");

            string scoord = node.InnerText;
            string[] split = scoord.Split(',');
            double lon = double.Parse(split[0], CultureInfo.InvariantCulture);
            double lat = double.Parse(split[1], CultureInfo.InvariantCulture);
            double alt = 0;
            if(split.Length > 2)
                alt = double.Parse(split[2], CultureInfo.InvariantCulture);
            KMLPoint point = new KMLPoint(altMode, lat, lon, alt);

            return point;
        }

        private static AltitudeMode GetAltMode(string saltMode)
        {
            switch (saltMode)
            {
                case "relativeToGround":
                    return AltitudeMode.RelativeToGround;
                default:
                    throw new Exception("Wrong mode");
            }
        }

        private static void AssertRead(bool p)
        {
            if (!p) throw new Exception("Wrong KML document");
        }

        public List<KMLFolder> Folders
        {
            get { return folders; }
        }
    }
}
